﻿// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the MIT license.  See License.txt in the project root for license information.

using System.Collections.Immutable;

using Analyzer.Utilities.Extensions;
using Analyzer.Utilities.PooledObjects;

using Microsoft.CodeAnalysis;

using MSTest.Analyzers.Helpers;

using Roslyn.Utilities;

namespace Analyzer.Utilities;

/// <summary>
/// Provides and caches well known types in a compilation.
/// </summary>
internal sealed class WellKnownTypeProvider
{
    private static readonly BoundedCacheWithFactory<Compilation, WellKnownTypeProvider> s_providerCache = new();

    private WellKnownTypeProvider(Compilation compilation)
    {
        Compilation = compilation;
        _fullNameToTypeMap = new ConcurrentDictionary<string, INamedTypeSymbol?>();
        _referencedAssemblies = new Lazy<ImmutableArray<IAssemblySymbol>>(
            () => Compilation.Assembly.Modules
                    .SelectMany(m => m.ReferencedAssemblySymbols)
                    .Distinct<IAssemblySymbol>(SymbolEqualityComparer.Default)
                    .ToImmutableArray(),
            LazyThreadSafetyMode.ExecutionAndPublication);
    }

    public static WellKnownTypeProvider GetOrCreate(Compilation compilation)
    {
        return s_providerCache.GetOrCreateValue(compilation, CreateWellKnownTypeProvider);

        // Local functions
        static WellKnownTypeProvider CreateWellKnownTypeProvider(Compilation compilation) => new(compilation);
    }

    public Compilation Compilation { get; }

    /// <summary>
    /// All the referenced assembly symbols.
    /// </summary>
    /// <remarks>
    /// Seems to be less memory intensive than:
    /// foreach (Compilation.Assembly.Modules)
    ///     foreach (Module.ReferencedAssemblySymbols)
    /// </remarks>
    private readonly Lazy<ImmutableArray<IAssemblySymbol>> _referencedAssemblies;

    /// <summary>
    /// Mapping of full name to <see cref="INamedTypeSymbol"/>.
    /// </summary>
    private readonly ConcurrentDictionary<string, INamedTypeSymbol?> _fullNameToTypeMap;

    /// <summary>
    /// Static cache of full type names (with namespaces) to namespace name parts,
    /// so we can query <see cref="IAssemblySymbol.NamespaceNames"/>.
    /// </summary>
    /// <remarks>
    ///
    /// Example: "System.Collections.Generic.List`1" => [ "System", "Collections", "Generic" ]
    ///
    /// https://github.com/dotnet/roslyn/blob/9e786147b8cb884af454db081bb747a5bd36a086/src/Compilers/CSharp/Portable/Symbols/AssemblySymbol.cs#L455
    /// suggests the TypeNames collection can be checked to avoid expensive operations. But realizing TypeNames seems to be
    /// as memory intensive as unnecessary calls GetTypeByMetadataName() in some cases. So we'll go with namespace names.
    /// </remarks>
    private static readonly ConcurrentDictionary<string, ImmutableArray<string>> _fullTypeNameToNamespaceNames = new();

    /// <summary>
    /// Attempts to get the type by the full type name.
    /// </summary>
    /// <param name="fullTypeName">Namespace + type name, e.g. "System.Exception".</param>
    /// <param name="namedTypeSymbol">Named type symbol, if any.</param>
    /// <returns>True if found in the compilation, false otherwise.</returns>
    [PerformanceSensitive("https://github.com/dotnet/roslyn-analyzers/issues/4893", AllowCaptures = false)]
    public bool TryGetOrCreateTypeByMetadataName(
        string fullTypeName,
        [NotNullWhen(returnValue: true)] out INamedTypeSymbol? namedTypeSymbol)
        => _fullNameToTypeMap.TryGetValue(fullTypeName, out namedTypeSymbol)
            ? namedTypeSymbol is not null
            : TryGetOrCreateTypeByMetadataNameSlow(fullTypeName, out namedTypeSymbol);

    private bool TryGetOrCreateTypeByMetadataNameSlow(
        string fullTypeName,
        [NotNullWhen(returnValue: true)] out INamedTypeSymbol? namedTypeSymbol)
    {
        namedTypeSymbol = _fullNameToTypeMap.GetOrAdd(
            fullTypeName,
            fullyQualifiedMetadataName =>
            {
                // Caching null results is intended.

                // sharwell says: Suppose you reference assembly A with public API X.Y, and you reference assembly B with
                // internal API X.Y. Even though you can use X.Y from assembly A, compilation.GetTypeByMetadataName will
                // fail outright because it finds two types with the same name.

                INamedTypeSymbol? type = null;

                ImmutableArray<string> namespaceNames = string.IsInterned(fullTypeName) != null
                    ? _fullTypeNameToNamespaceNames.GetOrAdd(
                        fullTypeName,
                        GetNamespaceNamesFromFullTypeName)
                    : GetNamespaceNamesFromFullTypeName(fullTypeName);
                // Assuming we're on .NET Standard 2.0 or later, cache the type names that are probably compile time constants.

                if (IsSubsetOfCollection(namespaceNames, Compilation.Assembly.NamespaceNames))
                {
                    type = Compilation.Assembly.GetTypeByMetadataName(fullyQualifiedMetadataName);
                }

                if (type is null)
                {
                    RoslynDebug.Assert(namespaceNames != null);

                    foreach (IAssemblySymbol? referencedAssembly in _referencedAssemblies.Value)
                    {
                        if (!IsSubsetOfCollection(namespaceNames, referencedAssembly.NamespaceNames))
                        {
                            continue;
                        }

                        INamedTypeSymbol? currentType = referencedAssembly.GetTypeByMetadataName(fullyQualifiedMetadataName);
                        if (currentType is null)
                        {
                            continue;
                        }

                        switch (currentType.GetResultantVisibility())
                        {
                            case SymbolVisibility.Public:
                            case SymbolVisibility.Internal when referencedAssembly.GivesAccessTo(Compilation.Assembly):
                                break;

                            default:
                                continue;
                        }

                        if (type is object)
                        {
                            // Multiple visible types with the same metadata name are present.
                            return null;
                        }

                        type = currentType;
                    }
                }

                return type;
            });

        return namedTypeSymbol != null;
    }

    /// <summary>
    /// Gets a type by its full type name.
    /// </summary>
    /// <param name="fullTypeName">Namespace + type name, e.g. "System.Exception".</param>
    /// <returns>The <see cref="INamedTypeSymbol"/> if found, null otherwise.</returns>
    public INamedTypeSymbol? GetOrCreateTypeByMetadataName(string fullTypeName)
    {
        TryGetOrCreateTypeByMetadataName(fullTypeName, out INamedTypeSymbol? namedTypeSymbol);
        return namedTypeSymbol;
    }

    /// <summary>
    /// Determines if <paramref name="typeSymbol"/> is a <see cref="Task{TResult}"/> with its type
    /// argument satisfying <paramref name="typeArgumentPredicate"/>.
    /// </summary>
    /// <param name="typeSymbol">Type potentially representing a <see cref="Task{TResult}"/>.</param>
    /// <param name="typeArgumentPredicate">Predicate to check the <paramref name="typeSymbol"/>'s type argument.</param>
    /// <returns>True if <paramref name="typeSymbol"/> is a <see cref="Task{TResult}"/> with its
    /// type argument satisfying <paramref name="typeArgumentPredicate"/>, false otherwise.</returns>
    internal bool IsTaskOfType([NotNullWhen(returnValue: true)] ITypeSymbol? typeSymbol, Func<ITypeSymbol, bool> typeArgumentPredicate)
        => typeSymbol != null
            && typeSymbol.OriginalDefinition != null
            && SymbolEqualityComparer.Default.Equals(typeSymbol.OriginalDefinition,
                GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksTask1))
            && typeSymbol is INamedTypeSymbol namedTypeSymbol
            && namedTypeSymbol.TypeArguments.Length == 1
            && typeArgumentPredicate(namedTypeSymbol.TypeArguments[0]);

    private static ImmutableArray<string> GetNamespaceNamesFromFullTypeName(string fullTypeName)
    {
        using var namespaceNamesBuilder = ArrayBuilder<string>.GetInstance();
        RoslynDebug.Assert(namespaceNamesBuilder != null);

        int prevStartIndex = 0;
        for (int i = 0; i < fullTypeName.Length; i++)
        {
            if (fullTypeName[i] == '.')
            {
                namespaceNamesBuilder.Add(fullTypeName[prevStartIndex..i]);
                prevStartIndex = i + 1;
            }
            else if (!IsIdentifierPartCharacter(fullTypeName[i]))
            {
                break;
            }
        }

        return namespaceNamesBuilder.ToImmutable();
    }

    /// <summary>
    /// Returns true if the Unicode character can be a part of an identifier.
    /// </summary>
    /// <param name="ch">The Unicode character.</param>
    private static bool IsIdentifierPartCharacter(char ch)
    {
        // identifier-part-character:
        //   letter-character
        //   decimal-digit-character
        //   connecting-character
        //   combining-character
        //   formatting-character

        if (ch < 'a') // '\u0061'
        {
            if (ch < 'A') // '\u0041'
            {
                return ch is >= '0'  // '\u0030'
                    and <= '9'; // '\u0039'
            }

            return ch is <= 'Z'  // '\u005A'
                or '_'; // '\u005F'
        }

        if (ch <= 'z') // '\u007A'
        {
            return true;
        }

        if (ch <= '\u007F') // max ASCII
        {
            return false;
        }

        UnicodeCategory cat = CharUnicodeInfo.GetUnicodeCategory(ch);

        return cat switch
        {
            // Letter
            UnicodeCategory.UppercaseLetter
            or UnicodeCategory.LowercaseLetter
            or UnicodeCategory.TitlecaseLetter
            or UnicodeCategory.ModifierLetter
            or UnicodeCategory.OtherLetter
            or UnicodeCategory.LetterNumber
            or UnicodeCategory.DecimalDigitNumber
            or UnicodeCategory.ConnectorPunctuation
            or UnicodeCategory.NonSpacingMark
            or UnicodeCategory.SpacingCombiningMark
            or UnicodeCategory.Format => true,
            _ => false,
        };
    }

    private static bool IsSubsetOfCollection<T>(ImmutableArray<T> set1, ICollection<T> set2)
    {
        if (set1.Length > set2.Count)
        {
            return false;
        }

        for (int i = 0; i < set1.Length; i++)
        {
            if (!set2.Contains(set1[i]))
            {
                return false;
            }
        }

        return true;
    }
}
